初學JavaScript的時候,其中一個難題就是靈活運用高階函數(Higher-order functions),例如forEach()
、map()
、filter()
、reduce()
等等。雖然看了文檔,但到了實做題目時,往往發現自己想不起原來這裏可以用某個方法,說白了就是用得不夠熟不夠多。所以今天就直接跳去刷Codewars題目來鍛練一下,難度都是入門程度(~7kyu)。
今天要練習是在Codewars collection這裏的第1-3題。因為篇幅問題,所以這裏就不貼整條問題,只會寫題目概要,重點是要跟大家分享自己的解法,以及討論一下別人的解法。
Coding Meetup #1 - Higher-Order Functions Series - Count the number of JavaScript developers coming from Europe
題目概要:找出來自歐洲及寫JavaScript
的開發者的人數。
var list1 = [
{ firstName: 'Noah', lastName: 'M.', country: 'Switzerland', continent: 'Europe', age: 19, language: 'JavaScript' },
{ firstName: 'Maia', lastName: 'S.', country: 'Tahiti', continent: 'Oceania', age: 28, language: 'JavaScript' },
{ firstName: 'Shufen', lastName: 'L.', country: 'Taiwan', continent: 'Asia', age: 35, language: 'HTML' },
{ firstName: 'Sumayah', lastName: 'M.', country: 'Tajikistan', continent: 'Asia', age: 30, language: 'CSS' }
];
這題比較簡單,很直覺會想到用filter
。
function countDevelopers(list) {
// your awesome code here :)
let result = list.filter( (person) => {
return person.continent === 'Europe' && person.language === 'JavaScript'
})
return result.length
}
把合乎條件的物件放到result
,再去找result
的長度。
雖然別人又會用filter
,但別人的寫法更簡潔:
const countDevelopers = list => (
list.filter(({continent, language}) => continent === 'Europe' && language === 'JavaScript').length
)
我沒有想過可以直接把屬性傳進去filter
函式裏去做判斷。
除此之外,還發現有人用reduce
去寫,如果有合乎條件的話,累加器参數就會加一。這樣做法我的確沒想過。自己對reduce
的了解還不夠,於是看懂了邏輯後再寫一次:
function countDevelopers(list){
return list.reduce( (acc,curr)=> curr.continent === 'Europe' && curr.language === 'JavaScript' ? acc + 1 : acc , 0)
}
關於reduce
的用法我不太熟,所以也查一下MDN
您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。
所以每次跑函式時所返回的值都會傳給累加器(即是reduce
函式第一個参數),在我的例子中就是acc
,最後得出的值就是是跑完reduce
函式後累加器的值。另外要注意的是,這裏設定了初始值是0,所以一開始acc
的值會是0,在迭代過程中,有找到合乎條件就會+1,沒有的話就返回原本累加器的值。
Coding Meetup #2 - Higher-Order Functions Series - Greet developers
題目概要:回傳一個在每個物件上,都有加上greeting
屬性的陣列。
var list1 = [
{ firstName: 'Sofia', lastName: 'I.', country: 'Argentina', continent: 'Americas', age: 35, language: 'Java' },
{ firstName: 'Lukas', lastName: 'X.', country: 'Croatia', continent: 'Europe', age: 35, language: 'Python' },
{ firstName: 'Madison', lastName: 'U.', country: 'United States', continent: 'Americas', age: 32, language: 'Ruby' }
];
回傳結果要變成這樣:
[
{ firstName: 'Sofia', lastName: 'I.', country: 'Argentina', continent: 'Americas', age: 35, language: 'Java',
greeting: 'Hi Sofia, what do you like the most about Java?'
},
...
];
function greetDevelopers(list){
return list.map( function(list) {
let greetObj = {greeting : `Hi ${list.firstName}, what do you like the most about ${list.language}?`}
let obj = {...list,...greetObj}
return obj
})
}
我的做法就是,先組合一個物件,再與目前被迭代的每一個物件合併,回傳一個個包含了屬性greeting
的物件到新陣例,並回傳這個陣列。
在寫出以上程式碼之前,也曾經失敗過一次。其實一開始的時候我是這樣寫的,但之後發現有問題:
function greetDevelopers(list){
return list.map( (list) => list.greeting = `Hi ${list.firstName}, what do you like the most about ${list.language}?`)
}
console.log(greetDevelopers(list1))
//["Hi Sofia, what do you like the most about Java?", "Hi Lukas, what do you like the most about Python?", "Hi Madison, what do you like the most about Ruby?"]
這裏回傳的是一個集合了greeting
屬性值的陣列,並不是我預期想要的陣列,為什麼呢?之後再仔細想想我的程式碼,其實攤長來寫是這樣:
function greetDevelopers(list){
return list.map( function(list){
return list.greeting = `Hi ${list.firstName}, what do you like the most about ${list.language}?`
})
}
console.log(greetDevelopers(list1))
這就清晰可見,因為我在map
函式裏所return的是list
裏面greeting
的屬性,所以最後回傳的是集個了greeting
的屬性值的陣列。
但是!在討論區裏其實有人寫了一個非常相似的做法,卻又能成功回傳我們想要的陣列:
function greetDevelopers(list) {
// thank you for checking out my kata :)
return list.map( function( a ) {
a.greeting = "Hi " + a.firstName + ", what do you like the most about " + a.language + "?";
return a; //與我的寫法不同,這裏是return a 這個值。
});
}
問題來了,我的寫法與它的差異就是,第一,我用了ES6的箭頭函式,他沒有用。第二,他在map
函式裏的最後一行是寫return a
,而我是returnlist.greeting
。
在這個解法的下面,有個網友跟我一樣問了同樣的問題,他也看出沒有用ES6,猜想背後是不是牽涉到this的概念問題。而他問題裏的寫法和結果跟我也一樣的,都是回傳一個只有greeting
屬性值的陣列:
其實這個問題與是否用ES6的寫法無直接關係,反而是與剛才提及到第二點,那個成功的寫法最後是return a
,而不是return 整個list.map
。
仔細想想這裏的return a
,那個a
就是我在greetDevelopers
函式的参數list
裏面的每一個的元素,即是原本list 1陣列裏的每一個元素。在list.map
函式裏,每個被迭代的物件都新增了greeting
這個屬性,所以我們要回傳的是這個被新增了greeting
屬性的物件,而非回傳greeting
屬性。
但事情還沒完,我之後再在全域查詢list1和greetDevelopers(list1):
function greetDevelopers(list) {
// thank you for checking out my kata :)
return list.map( function( a ) {
a.greeting = "Hi " + a.firstName + ", what do you like the most about " + a.language + "?";
return a; //與我的寫法不同,這裏是return a 這個值。
});
}
console.log(greetDevelopers(list1)) //印出greetDevelopers產生出來的新陣列
console.log(list1) //印出原陣列list1
發現list1竟然被修改了!內容跟greetDevelopers(list)所回傳的結果是一樣!!但看文檔時,不是說好了map
不會修改原陣列嗎??看看console印出來的結果:
上面是新陣列,下面是舊陣列。
竟然變成一樣!!用map
方法去處理陣列,最後竟然修改了陣列...(無奈
之後查一下,原來有人在stackoverflow問相似的問題。真是一言驚醒夢中人:
You're not modifying your original array. You're modifying the objects in the array.
之後也有網友補充:
...if you were to === the two arrays, you would find that they would not be equal (not same address in memory) confirming that the mapped array is in fact a new array. The issue is that you're returning a new array, that is full of references to the SAME objects in the original array (it's not returning new object literals, it's returning references to the same object). So you need to be creating new objects that are copies of the old objects - ie, w/ the Object.assign example given by SimpleJ.
簡單講就是我並沒有修改陣列,但我的確修改了陣列裏面的物件。在新陣列裏的每一個物件的記憶體的地址都是與舊陣列裏的每一個物件的記憶體的地址相同。根據傳址的概念,一旦修改了物件,所有擁有該物件地址的值都會被修改。
果然如此,當我試試這樣寫:
console.log(greetDevelopers(list1) === list1) //false
的確證明了map
沒有修改到原陣列。只不過因為我在map
產生新陣列時,修改了物件,所以不管是新陣列還是舊陣列,裏面的物件都會一併被修改。
沒有。
因為我的做法並沒有修改物件。我的做法是建立一個新物件,把每一個在舊陣列的物件與有greeting
屬性的物件合併,再把該新物件塞進map
所產生的新陣列中。
看完另一個網友的寫法後,發現寫法原來可以更簡潔一點:
const greetDevelopers = list => list.map(
dev => ({...dev, greeting: `Hi ${dev.firstName}, what do you like the most about ${dev.language}?`})
);
直接在展開每一個被迭代的物件的後面,加插greeting
屬性,並回傳整個物件,拆開來寫就是這樣:
const greetDevelopers = function(list){
return list.map( (dev) => {
return {...dev,greeting: `Hi ${dev.firstName}, what do you like the most about ${dev.language}?`}
})
}
Coding Meetup #3 - Higher-Order Functions Series - Is Ruby coming?
第3題挺簡單的,就是如果整個list裏面,有其中一個開發者是Ruby開發者,就回傳true
。這個條件讓人很易想起用some
這個方法。
var list1 = [
{ firstName: 'Emma', lastName: 'Z.', country: 'Netherlands', continent: 'Europe', age: 29, language: 'Ruby' },
{ firstName: 'Piotr', lastName: 'B.', country: 'Poland', continent: 'Europe', age: 128, language: 'Javascript' },
{ firstName: 'Jayden', lastName: 'P.', country: 'Jamaica', continent: 'Americas', age: 42, language: 'JavaScript' }
];
function isRubyComing(list) {
// thank you for checking out my kata :)
return list.some( list => list.language === 'Ruby' ? true : false)
}
太直覺地寫,沒發覺後面的true和false是多餘的XD,最簡單寫法應該是:
function isRubyComing(list) {
// thank you for checking out my kata :)
return list.some( list => list.language === 'Ruby')
}
快速看看別人的答案時,還真的有人用filter
去寫,例如這樣:
function isRubyComing(list){
return list.filter( (dev) => dev.language === 'Ruby').length > 0 ? true : false
}
用filter
產生一個屬性language
是Ruby
的陣列,如果長度大於1就回傳true
。
用刷題的方法來學習高階函數的確是好玩的,有時候submit答案後,看看別人的寫法,才發現自己寫了多餘的程式碼和用了較慢的方法,也有機會理解更多一些未曾想過的方法。踩坑時又能碰碰運氣看其他網友有沒有提出我心裏正想問的問題,難怪這麼多人推Codewars!